﻿using System;
using System.Collections.Generic;
using System.Windows.Forms;

/*
	Todo:
	make baseRowconverter overridable and support nulls

	work on a cannonical data-entry application with all the trimmings:
		- base form class with overrides.
		- when invalid set focus to bad
		- validate record as a whole with when invalid set focus to bad
		- decode codes via list and combo boxes
*/



namespace SimpleEntry
{
	// Interface to the underlying data. Actually now only required for the Name property
	public interface IDataElement
	{
		// get/set the data object
		object Value {get; set;}
		// get/set a friendly name for error messages, input labels etc
		String Name {get; set;}
	}
	
	// Interface to wrap the interaction between a data item and a Windows control
	public interface IEditControl
	{
		// get/set a string representation of the actual value to bind to the control
		// Note: set for a list box should set the selection.
		String Value {get; set;}
		// It may be useful for a form the lookup a value from a form control
		// this should return the windows form associated with the data
		// It is anticipated that objects implementing this interface will attach the control
		// in the object constructor rather than use this method (See the RadioGroup problem)
		Control FormControl {get; set;}
	}
	
	/// <summary>
	/// interface to manage data transfer between edit and data. Contains a Format method for 
	/// creating a string representation of the data, a Parse method for creating a data object
	/// from an input string and a validate method for checking whether or not the object value is
	/// valid for data being represented (eg. Age between 0 anf 150!)
	/// </summary>
	public interface IConverter
	{
		/// <summary>
		/// Create a string representation of the given object. 
		/// (eg the most basic format is o.ToString()) 
		/// </summary>
		/// <param name="o">The object to format into a string </param>
		/// <returns>The string represenation of the object</returns>
		String Format(object o);

		/// <summary>
		/// Convert a string to an object if possible. If not returns null and sets the error string. 
		/// Set the error string to empty if the string is parsed ok.
		/// assume the error string length is tested for 0 for a successful parse as a null object 
		/// may be a legitimate value.
		/// </summary>
		/// <param name="value">The String to parse</param>
		/// <param name="parseErrorMessage">
		/// The parse error message if any. Otherwise an empty string if the string
		/// was successfully parsed.
		/// </param>
		/// <returns></returns>
		object Parse(String value, out String parseErrorMessage);

	}
	/// <summary>
	/// Interface to provide validation of an object. The idea is that individual validators
	/// can be chained together to build a set of rules for a particular item.
	/// </summary>
	public interface IValidater
	{
		/// <summary>
		/// Perform any required validation tests on the object.
		/// eg string length, number range, date range etc.
		/// </summary>
		/// <param name="o">The object to validate</param>
		/// <returns>True if the object passes validation. False otherwise.</returns>
		bool Validate(object o);

		/// <summary>
		/// The error message from the last call to Validate (an empty string if
		/// the object is valid). The recommended pattern of use is to fetch the 
		/// message immediately after detecting an invalid object in case the validater is
		/// applied to more than one object
		/// </summary>
		String LastError {get;}
		
		/// <summary>
		/// Sets the next validater in the chain. The current one is inovked first
		/// then invokes it's single child. The BaseValidater implementation will handle this
		/// </summary>
		/// <param name="nextValidator">The next validater object to apply</param>
		/// <returns>The nextValidtor object so the client program can use chaining</returns>
		IValidater SetNext(IValidater nextValidater);
	}

	/// <summary>
	/// Base class created by framework facade code when no validation is required for 
	/// a field. Base class functionality provides an public property for the error
	/// so inherited classes don't need to manage that.
	/// </summary>
	public class BaseValidater : IValidater
	{	
		// Interface 
		public bool Validate(object o)
		{
			// this object fails validation
			if (!InternalValidate(o, out m_LastError)) return false;
			// for validated ok clear any held message
			this.m_LastError = "";
			// call next in line
			if (this.m_Next == null || m_Next.Validate(o)) return true;
			// propagates the error message up the chain. Not brilliant (sad face)
			this.m_LastError = m_Next.LastError;
			return false; // failed validation somewhere in the chain
		}

		public String LastError { get { return m_LastError; } }

		public IValidater SetNext(IValidater nextValidater)
		{
			if (this.m_Next == null)
				this.m_Next = nextValidater;
			else
				this.m_Next.SetNext(nextValidater);
			return this;
		}

		///////////////////////////////////
		protected virtual bool InternalValidate(object o, out String errorMessage)
		{
			// this implementation never generates an error.
			errorMessage = ""; 
			// always valid.
			return true;
		}

		/// <summary>
		/// Helper method because very often the true validation is not sensible
		/// for an empty method. 
		/// </summary>
		/// <param name="o"></param>
		/// <returns></returns>
		protected bool IsEmpty(object o)
		{
			if (o != null)
				if (o.GetType() != typeof(System.DBNull))
					if (o.ToString().Length > 0)
						return false;
			return true;
		}
		
		String m_LastError = "";
		IValidater m_Next = null;
	}
	
	//////////////////////////////////////////////////////////////////////////////////////////
	// Class to implement generic functionality in binding a data element to a winforms control
	// 
	/// <summary>
	/// Class to associate a piece of data to a windows control with 
	/// a converter to transfer the data to and from the control
	/// </summary>
	public class EntryBinding
	{
		/// <summary>
		/// Full control constructor.
		/// </summary>
		/// <param name="data">Interface representing a piece of data to modify</param>
		/// <param name="control">Interface to a Window control.</param>
		/// <param name="converter">Interface to an object that controls data exchange</param>
		/// <param name="validator">Interface to an object that performs validation.</param>
		public EntryBinding(IDataElement data, IEditControl control, IValidater validator, IConverter converter)
		{
			initialise(data, control, validator, converter);
		}
/*
		/// <summary>
		/// Legacy constructor. Use UniversalConverter for known datatypes.
		/// </summary>
		/// <param name="data">Interface representing a piece of data to modify</param>
		/// <param name="control">Interface to a Window control.</param>
		/// <param name="converter">Interface to an object that controls data exchange</param>
		public EntryBinding(IDataElement data, IEditControl control, IConverter converter)
		{
			initialise(data, control, null, converter);
		}

		public EntryBinding(IDataElement data, IEditControl control, IValidater validater)
		{
			initialise(data, control, validater, null);
		}
*/
		/// <summary>
		/// Lite constructor. supplies a default converter, which does not special validation
		/// </summary>
		/// <param name="data">Interface representing a piece of data to modify</param>
		/// <param name="control">Interface to an object that controls formatting and validation.</param>
		public EntryBinding(IDataElement data, IEditControl control)
		{
			initialise(data, control, null, null);
		}
		
		// object access properties.
		/// <summary>
		/// Access the data element of the binding
		/// </summary>
		public IDataElement Data { get { return m_Data; } set {m_Data = value; }}
		/// <summary>
		/// Access the Control of the binding
		/// </summary>
		public IEditControl Control { get { return m_Control; } set { m_Control = value; } }
		/// <summary>
		/// Access the Formatter interface of the binding
		/// </summary>
		public IConverter Converter { get { return m_Converter; } set { m_Converter = value; } }
		/// <summary>
		/// Access the Validater interface of the binding
		/// </summary>
		public IValidater Validater { get { return m_Validater; } set { m_Validater = value; } }

		// Data Exchange. Two directions: from data to control; from control to data
		/// <summary>
		/// Binding data exchange directions. Either to control from data or back to data from control.
		/// </summary>
		public enum BindingDirection {ToControl, FromControl}

		/// <summary>
		/// Update the control from the data or the data from the control.
		/// Typically use ToControl when loading the edit form and FromControl
		/// when the user opts to save changes.
		/// </summary>
		/// <param name="direction">Either to control or from control</param>
		public void DataExchange(BindingDirection direction)
		{
			if (direction == BindingDirection.ToControl)
			{
				// get from element
				object o = m_Data.Value;
				String display = m_Converter.Format(o);
				this.m_Control.Value = display;
			}
			else if (direction == BindingDirection.FromControl)
			{
				String display = m_Control.Value;
				// what if it isn't
				try
				{
					String message;
					object o = m_Converter.Parse(display, out message);
					this.m_Data.Value = o;
				}
				catch (Exception exc)
				{
					Console.WriteLine(exc.Message);
				}
				
			}
		}
		
		/// <summary>
		/// Validation uses the converter.
		/// Step 1. Can the string supplied by the control actually be parsed to create
		///         an instance of the object type of the data element. 
		///         eg. 99/999 won't create a DateTime object.
		/// Step 2. is the value of the object valid for this particular binding?
		///         eg. a number in the correct range or a string that is not too long.
		/// </summary>
		/// <returns>True if input is valid. False otherwise</returns>
		public bool ValidateInput()
		{
			// get display from string
			String display = m_Control.Value;
			// parse the string input to the expected object type
			object o = m_Converter.Parse(display, out lastError);
			if (lastError.Length > 0)
				return false; // failed parse step (Step 1 above)

			// validate the object
			// above has reset lastError to empty
			if (this.m_Validater.Validate(o)) return true;
			// oh dear its not valid so get the reason.
			lastError = m_Validater.LastError;
			return false;
		}
		
		/// <summary>
		/// Access the last error generated by the validation process
		/// </summary>
		/// <returns>The error message text.</returns>
		public String GetLastError() { return lastError; }

		///////////////////////////////////////////////////////
		private void initialise(IDataElement data, IEditControl control, IValidater validator, IConverter converter)
		{
			this.m_Data = data;
			this.m_Control = control;
			this.m_Converter = converter;
			this.m_Validater = validator;
			if (this.m_Converter == null) m_Converter = new UniversalConverter(data.Value.GetType());
			if (this.m_Validater == null) m_Validater = new BaseValidater();
		}


		String lastError = "";
			
		IDataElement m_Data;
		IEditControl m_Control;
		IConverter  m_Converter;
		IValidater m_Validater;
	}
	
	/// <summary>
	/// Class to manage a collection of entry bindings
	/// All the Add overrides are a bit of mess that reflects a change of design and
	/// an attempt to keep legacy code working. This shold be tidied.
	/// </summary>
	public class EntryBindings
	{
		/// <summary>
		/// Create a binding and add to the collection using standard mappings of data 
		/// and controls. Note there may not be default mappings available for all controls
		/// or object data types. This will cause an exception. In these cases further custom
		/// classes will need to be created. This version creates a default converter.
		/// </summary>
		/// <param name="editControl">Windows control to wrap with a control interface</param>
		/// <param name="o">Data item to act act on that will be wrapped in a default data element interface.</param>
		/// <param name="name">A friendly name for the data item (for error messages)</param>
		public void Add(Control editControl, object o, String name)
		{
			Add(editControl, o, name, new BaseValidater());
		}

		/// <summary>
		/// Create a binding and add to the collection using standard mappings of data 
		/// and controls. Note there may not be default mappings available for all controls
		/// or object data types. This will cause an exception. In these cases further custom
		/// classes will need to be created. This version requires a pre-constructed converter and 
		/// can therefore be used for standard data types and controls, but where specialised 
		/// validation (eg number or data range checks) need to be applied.
		/// </summary>
		/// <param name="editControl">Windows control to wrap with a control interface</param>
		/// <param name="o">Data item to act act on that will be wrapped in a default data element interface.</param>
		/// <param name="name">A friendly name for the data item (for error messages)</param>
		/// <param name="converter">a pre-constructed converter.</param>
		public void Add(Control editControl, object o, String name, IValidater validater)
		{
			DataElement data = new DataElement(o, name);
			IEditControl control = MakeEditControl(editControl);
			// all there to add now
			//Add(data, control, validater, null);
			DataBindingList.Add(new EntryBinding(data, control, validater, null));
		}
		
		 /// <summary>
		/// Add a data and control binding with a converter
		/// </summary>
		/// <param name="data">Interface representing a piece of data to modify</param>
		/// <param name="control">Interface to a Window control.</param>
		/// <param name="validater">Interface to an object that controls validation. May be null to use a default.</param>
		/// <param name="converter">Interface to an object that controls formatting. May be null to use a default</param>
		public void Add(IDataElement data, IEditControl control, IValidater validater, IConverter converter)
		{
			EntryBinding bind = new EntryBinding(data, control, validater, converter);
			DataBindingList.Add(bind);
		}
		/// <summary>
		/// Add a pre-existing binding to the collections
		/// </summary>
		/// <param name="bind">A pre-constructed binding.</param>
		public void Add(EntryBinding bind) { DataBindingList.Add(bind); }

		/// <summary>
		/// Initialise the controls in the binding collection from the data. Typically this
		/// will be called when the form is first shown (the Load event handler).
		/// </summary>
		public void SetControls()
		{
			foreach (EntryBinding bind in DataBindingList)
				bind.DataExchange(EntryBinding.BindingDirection.ToControl);
		}

		// update data from controls 
		/// <summary>
		/// Update the data in the binding collection from the control settings. Typically
		/// this will be called when the user saves his edits, but after the input has been 
		/// validated.
		/// </summary>
		public void UpdateData()
		{
			foreach (EntryBinding bind in DataBindingList)
				bind.DataExchange(EntryBinding.BindingDirection.FromControl);
		}
		
		/// <summary>
		/// Check whether updating the data from the controls would change any data.
		/// Use to test whether it is safe to cancel an entry dialog.
		/// </summary>
		/// <returns>True if there has been some change. False if not.</returns>
		public bool IsDirty()
		{
			bool editMade = false;
			foreach (EntryBinding bind in DataBindingList)
			{
				String display = bind.Control.Value;
				String message;
				object o = bind.Converter.Parse(display, out message);
				if (!bind.Data.Value.Equals(o))
					editMade = true;
			}
			return editMade;
		}

		//////////////// Validation /////////////////////////////////////////////
		// 
		
		/// <summary>
		/// Form validation callback. Checks for invalid combinations of inputs.
		/// Work on the control state as the data should not have been updated yet
		/// </summary>
		/// <param name="bindings">List of bindings containing the form state to check</param>
		/// <param name="errorBinding">out parameter containing a/the binding with an/the error</param>
		/// <param name="errorMessage">out parameter. A meainingful error message</param>
		/// <returns></returns>
		public delegate bool FormValidate(EntryBindings bindings, out EntryBinding errorBinding, out String errorMessage);

		/// <summary>
		/// The form validation callback property. Stays null implying no global
		/// form validation unless set by the calling program.
		/// </summary>
		public FormValidate FormValidation {get; set; }
		
		// validation options	
		/// <summary>
		/// Options for validation errors. Either stop validation on the first
		/// error or continue to create a list of all errors.
		/// </summary>
		public enum OnInputError {Stop, Continue}

		/// <summary>
		/// Validate the data in the controls using the converter.
		/// Step 1. Can the string supplied by the control actually be parsed to create
		///         an instance of the object type of the data element. 
		///         eg. 99/999 won't create a DateTime object.
		/// Step 2. is the value of the object valid for this particular binding?
		///         eg. a number in the correct range or a string that is not too long.
		/// </summary>
		/// <param name="errorAction">
		/// Determines what to when a validation error occurs. Options are
		/// either to stop at the first error or continue.</param>
		/// <returns></returns>
		public bool DoValidation(OnInputError errorAction)
		{
			// clear anything from the previous validation attempt
			this.m_ErrorMessages.Clear();
			this.m_ErrorBindings.Clear();
			// look on the bright side!
			bool validatedOk = true;
			foreach (EntryBinding bind in DataBindingList)
			{
				if (bind.ValidateInput() == false)
				{
					this.m_ErrorMessages.Add(String.Format("Input Error in {0}\n\n{1}", bind.Data.Name, bind.GetLastError()));
					this.m_ErrorBindings.Add(bind);
					validatedOk = false;
					// end of action if the stop option set
					if (errorAction == OnInputError.Stop)
						return false;
				}
			}
			// apply record validation if individual items pass validation
			// and the callback has been set
			if (validatedOk && FormValidation != null)
			{
				EntryBinding errorBinding;
				String errorMessage;
				validatedOk = FormValidation(this, out errorBinding, out errorMessage);
				if (!validatedOk)
				{
					this.m_ErrorMessages.Add(errorMessage);
					this.m_ErrorBindings.Add(errorBinding);
				}
			}
			return validatedOk;
		}
		
		/// <summary>
		/// Delegate to act as a callback when input is validated immediately the control
		/// loses focus. The parameter passed is the binding of the object that failed validation
		/// The LastError property should contain an appropriate error message to display
		/// </summary>
		/// <param name="errorBinding"></param>
		public delegate void ImmediateValidationError(EntryBinding errorBinding);

		/// <summary>
		/// Activates immediate validation of input to a control when the control loses focus.
		/// More accurately it is when another control in the binding list gains focus.
		/// This prevents problematical situations such as unwanted validation when the cancel
		/// key is pressed.
		/// </summary>
		/// <param name="callback">callback function to be invoked when input fails validation</param>
		public void SetImmediateValidation(ImmediateValidationError callback)
		{
			// need to hook up each control to a getfocus event handler.
			foreach (EntryBinding bind in DataBindingList)
			{
				bind.Control.FormControl.Enter += new EventHandler(gotFocus);
			}
			this.m_Callback = callback;
		}

		/// <summary>
		/// Recommended to be called on Dispose of the form to unhook handlers
		/// from the form. It should be ok without, but this may change when custom
		/// objects that implement the entry framework interfaces are used
		/// </summary>
		public void ClearImmediateValidation()
		{
			foreach (EntryBinding bind in DataBindingList)
			{
				bind.Control.FormControl.Enter -= new EventHandler(gotFocus);
			}
			this.m_Callback = null;
		}

		/// <summary>
		/// Factory method to map a Windows control to an wrapper object that implements the IEditControl
		/// interface. Currently supports TextBox, Label, ComboBox, ListBox and CheckBox
		/// </summary>
		/// <param name="control">The Winforms control to wrap</param>
		/// <returns>The created wrapper object cast to an IEditControl interface</returns>
		public static IEditControl MakeEditControl(Control control)
		{
			if (control.GetType() == typeof(TextBox)) return new TextEditControl(control as TextBox);
			if (control.GetType() == typeof(Label)) return new LabelControl(control as Label);
			if (control.GetType() == typeof(ComboBox)) return new ComboEditControl(control as ComboBox);
			if (control.GetType() == typeof(ListBox)) return new ListEditControl(control as ListBox);
			if (control.GetType() == typeof(CheckBox)) return new CheckControl(control as CheckBox);

			throw new Exception(String.Format("Cant a control of type {0}", control.GetType().ToString()));
		}

		// Hold validation errors
		List<String> m_ErrorMessages = new List<string>();
		List<EntryBinding> m_ErrorBindings = new List<EntryBinding>();

		// Error Accessors. 
		// single error properties
		public String ErrorMessage { get { return m_ErrorMessages[0]; } }
		public EntryBinding ErrorBinding { get { return m_ErrorBindings[0]; } }
		// multiple errors properities
		public int ErrorCount { get { return m_ErrorMessages.Count; } }
		public String[] ErrorMessages { get { return m_ErrorMessages.ToArray(); } }
		public EntryBinding[] ErrorBindings { get { return m_ErrorBindings.ToArray(); } }
		
		/// <summary>
		/// Indexer returns a binding given a winforms control. The EntryBindings class
		/// is intended to be used during form processing, so it makes more sense to index
		/// on the windows form object than anything else.
		/// </summary>
		/// <param name="control">winforms control</param>
		/// <returns>The binding object associated to the control</returns>
		public EntryBinding this[Control control]
		{
			get
			{
				foreach (EntryBinding bind in DataBindingList)
					if (bind.Control.FormControl == control) return bind;
				return null; 
			}
		}

		// focus tracker that is attached to all controls in the binding
		// activated when a control gets focus, it validates the previous control
		// and sets the previous control to the current control.
		// Beware: the client will commonly attempt to reset focus to the error control
		// so there are checks in the code to prevent a loop or double validation.
		private void gotFocus(object sender, EventArgs e)
		{
			// get the control that receives focus
			Control focusControl = sender as Control;
			// test whether conditions are right for validation
			if (this.m_LastKnownControl != null && m_LastKnownControl != focusControl)
			{
				// look up the binding from the winforms control
				EntryBinding b = this[this.m_LastKnownControl];
				if (b != null) // don't know why this should ever be null, but just in case
				{
					// main point is to report errors so make the test
					if (!b.ValidateInput())
					{
						// here if validation fails. Inform the client of the failure
						if (this.m_Callback != null)
							this.m_Callback(b);
					}
					else 
					{
						// here if it is OK. Take the opportunity to reformat the users input
						String userInput = b.Control.Value;  // value as just entered
						String errm;
						// convert and format (hope its not too expensive)
						String formattedData = b.Converter.Format(b.Converter.Parse(userInput, out errm));
						// update the control if necessary
						if (formattedData != userInput)
							b.Control.Value = formattedData;
					}
				}
			}
			// update last control with focus to one receiving it.
			this.m_LastKnownControl = focusControl;
		}
		
		List<EntryBinding> DataBindingList = new List<EntryBinding>();
		Control m_LastKnownControl = null;
		ImmediateValidationError m_Callback;
	}
	
}
